package com.vision.sales.sv.serv.rpc;

import static com.vision.sales.cm.data.SalesPermissions.CreateSalesInvoice;
import static com.vision.sales.cm.data.SalesPermissions.DeleteSalesInvoice;
import static com.vision.sales.cm.data.SalesPermissions.UpdateSalesInvoice;
import static com.vision.sales.cm.data.SalesPermissions.ViewSalesInvoice;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.sencha.gxt.data.shared.loader.PagingLoadResult;
import com.sencha.gxt.data.shared.loader.PagingLoadResultBean;
import com.vision.core.cm.data.Domain;
import com.vision.core.cm.db.data.Company;
import com.vision.core.cm.exception.RpcException;
import com.vision.core.sv.db.mgr.IDataManager;
import com.vision.core.sv.db.mgr.ISimpleCriteria;
import com.vision.core.sv.db.mgr.SimpleCriteria;
import com.vision.core.sv.util.CollectionUtils;
import com.vision.core.sv.util.SimpleTransformer;
import com.vision.sales.cl.ui.component.TransactionLoadConfig;
import com.vision.sales.cm.data.CollectionStatus;
import com.vision.sales.cm.data.SalesPermissions;
import com.vision.sales.cm.data.TransactionStatus;
import com.vision.sales.cm.data.TransactionType;
import com.vision.sales.cm.db.data.AdvancePayment;
import com.vision.sales.cm.db.data.Collection;
import com.vision.sales.cm.db.data.CollectionItem;
import com.vision.sales.cm.db.data.Customer;
import com.vision.sales.cm.db.data.DeliveryReceipt;
import com.vision.sales.cm.db.data.Product;
import com.vision.sales.cm.db.data.SalesInvoice;
import com.vision.sales.cm.db.data.Salesman;
import com.vision.sales.cm.db.data.SalesmanProduct;
import com.vision.sales.cm.db.data.TransactionEntry;
import com.vision.sales.cm.serv.rpc.SalesInvoiceService;
import com.vision.sales.sv.db.mgr.SalesmanProductManager;
import com.vision.sales.sv.util.TransactionHelper;

/**
 * 
 * @author Mark
 *
 */
@Service("sales_invoice")
public class SalesInvoiceServiceImpl extends TransactionServiceImpl<SalesInvoice> implements SalesInvoiceService {

	private static final long serialVersionUID = 1L;
	
	private static final Logger log = LoggerFactory.getLogger(SalesInvoiceServiceImpl.class);
	
	@Autowired
	@Qualifier("customerManager")
	private IDataManager<Customer> customerManager;
	
	@Autowired
	@Qualifier("salesmanManager")
	private IDataManager<Salesman> salesmanManager;
	
	@Autowired
	@Qualifier("productManager")
	private IDataManager<Product> productManager;
	
	@Autowired
	@Qualifier("salesmanProductManager")
	private SalesmanProductManager salesmanProductManager;
	
	@Autowired
	@Qualifier("deliveryReceiptManager")
	private IDataManager<DeliveryReceipt> drManager;
	
	@Autowired
	@Qualifier("collectionManager")
	private IDataManager<Collection> collectionManager;
	
	@Autowired
	@Qualifier("advancePaymentManager")
	private IDataManager<AdvancePayment> advancePaymentManager;
	
	@Autowired
	@Qualifier("transactionEntryManager")
	private IDataManager<TransactionEntry> transactionEntryManager;

	public SalesInvoiceServiceImpl() {
		initGilead();
	}
	
	@Autowired
	@Qualifier("salesInvoiceManager")
	@Override
	public void setDataManager(IDataManager<SalesInvoice> dataManager) {
		super.setDataManager(dataManager);
	}
	
	@Override
	protected Domain getDomain() {
		return Domain.SALES_INVOICE;
	}
	
	// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	@Override
	public List<SalesInvoice> getAll() throws RpcException {
		checkPermissions(ViewSalesInvoice);
		
		return super.getAll();
	}

	@SuppressWarnings({ "unchecked" })
	@Override
	public List<SalesInvoice> getInvoiceNoAndIdOnly() throws RpcException {
		checkPermissions(ViewSalesInvoice);
		
		ISimpleCriteria crit = SimpleCriteria.forClass(SalesInvoice.class);
		String[] properties = new String[]{ "id", "group", "invoiceNo", "remarks" };
		crit.properties(properties); // Retrieve only id, code, and remarks
		crit.asc("invoiceNo");
		crit.setResultTransformer(SimpleTransformer.create(SalesInvoice.class, properties));
		
		List<SalesInvoice> list = getTemplate().findByCriteria(crit.getDetached());
		return list;
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public CollectionItem getSalesInvoiceSummary(long salesInvoiceId) throws RpcException {
		checkPermissions(ViewSalesInvoice);
		
		SalesInvoice invoice = getDataManager().getById(salesInvoiceId);
		if (invoice == null) throw new RpcException("The sales invoice does not exist.");
		
		ISimpleCriteria crit = SimpleCriteria.forClass(CollectionItem.class);
		crit.eq("salesInvoice.id", salesInvoiceId);
		List<CollectionItem> collectionItems = getTemplate().findByCriteria(crit.getDetached());
		List<Long> transactionIds = CollectionUtils.listFrom(collectionItems, "getId");
		
		CollectionItem item = new CollectionItem();
		item.setAmount(0d);
		item.setOverruns(0d);
		item.setOverpayments(0d);
		item.setDiscounts(0d);
		item.setRejects(0d);
		item.setReturns(0d);
		item.setTax(0d);
		
		if (transactionIds.isEmpty())
			return item;
		
		crit = SimpleCriteria.forClass(TransactionEntry.class);
		crit.in("transactionId", transactionIds);
		
		List<TransactionEntry> list = getTemplate().findByCriteria(crit.getDetached());
		
		for (TransactionEntry entry : list) {
			switch (entry.getType()) {
			case COLLECTION_PAYMENT:
				item.setAmount(item.getAmount() + entry.getDebit() - entry.getCredit());
				break;
			case COLLECTION_OVERRUNS:
			case DEBIT_MEMO_OVERRUNS:
				item.setOverruns(item.getOverruns() + entry.getDebit() - entry.getCredit());
				break;
			case COLLECTION_OVERPAYMENT:
			case DEBIT_MEMO_OVERPAYMENTS:
				item.setOverpayments(item.getOverpayments() + entry.getDebit() - entry.getCredit());
				break;
			case COLLECTION_DISCOUNTS:
			case CREDIT_MEMO_DISCOUNTS:
				item.setDiscounts(item.getDiscounts() + entry.getDebit() - entry.getCredit());
				break;
			case COLLECTION_REJECTS:
			case CREDIT_MEMO_REJECTS:
				item.setRejects(item.getRejects() + entry.getDebit() - entry.getCredit());
				break;
			case COLLECTION_RETURNS:
			case CREDIT_MEMO_RETURNS:
				item.setReturns(item.getReturns() + entry.getDebit() - entry.getCredit());
				break;
			case COLLECTION_TAX:
				item.setTax(item.getTax() + entry.getDebit() - entry.getCredit());
				break;
			}
		}
		
		return item;
	}

	@Transactional
	@Override
	public SalesInvoice create(SalesInvoice invoice) throws RpcException {
		checkPermissions(CreateSalesInvoice);
		checkHasCompany(invoice.getCompany());
		
		// 20130504 - Changed specs to support creating of invoices w/out delivery receipts
		// as per meeting with Leonie on 20130502.
//		log.debug("Validating invoice...");
//		if (invoice.getDeliveryReceipts() == null || invoice.getDeliveryReceipts().isEmpty())
//			throw new RpcException("The sales invoice must have at least one delivery receipt.");
		
		checkFieldUnique("invoiceNo", invoice.getInvoiceNo(), "Invoice No.");
		
		Customer customer = customerManager.getById(invoice.getCustomer().getId());
		if (customer == null) throw new RpcException("The customer does not exist.");
		
		Salesman salesman = salesmanManager.getById(customer.getSalesman().getId());
		if (salesman == null) throw new RpcException("The salesman does not exist.");
		
		Product product = productManager.getById(invoice.getProduct().getId());
		if (product == null) throw new RpcException("The product does not exist.");
		
		log.debug("Setting pre-set values...");
		invoice.setSalesman(salesman);
		invoice.setBalance(invoice.getGrossSales());
		invoice.setCollectionStatus(CollectionStatus.UNPAID);
		invoice.setStatus(TransactionStatus.PENDING);

		// Set the commission rate
		SalesmanProduct sp = salesmanProductManager.get(salesman.getId(), product.getId());
		if (sp == null) {
			log.warn("Commission rate for salesman {} and product {} was not found. 0% will be used.", new Object[]{ salesman.getCode(), product.getCode() });
			invoice.setCommissionRate(0D);
		} else {
			// NOTE commission rate is not in % representation so we divide by 100
			invoice.setCommissionRate(sp.getCommissionRate() / 100D);
		}
		
		log.debug("Validating delivery receipts...");
		// Check the DeliveryReceipts
		List<Long> drIds = CollectionUtils.listFrom(invoice.getDeliveryReceipts(), "getId");
		List<DeliveryReceipt> existingDRs = Collections.emptyList();
		if (!drIds.isEmpty()) {
			existingDRs = drManager.getByIds(DeliveryReceipt.COL_ID, drIds);
			for (DeliveryReceipt dr : existingDRs) {
				if (!dr.getCustomer().equals(customer))
					throw new RpcException("One of the delivery receipts has a customer different from what the sales invoice has.");
				
				if (dr.getSalesInvoice() != null && dr.getSalesInvoice().getId() != null)
					throw new RpcException("One of the delivery receipts has already been invoiced.");
				
				if (dr.getDate().after(invoice.getDate()))
					throw new RpcException("One of the delivery receipts has a date later than the sales invoice's.");
			}
		}
		
		log.debug("Saving to database...");
		// Insert the SalesInvoice into the database
		SalesInvoice newInvoice = super.create(invoice);

		if (!existingDRs.isEmpty()) {
			log.debug("Updating delivery receipts...");
			// Set the SalesInvoice reference of the DeliveryReceipts
			for (DeliveryReceipt receipt : existingDRs) {
				receipt.setSalesInvoice(newInvoice);
				drManager.update(receipt);
			}
			newInvoice.setDeliveryReceipts(new HashSet<DeliveryReceipt>(existingDRs));
		}
		
		log.debug("Updating customer's balance...");
		// Update Customer's balance
		Double balance = customer.getBalance();
		if (balance == null) balance = 0d;
		customer.setBalance(balance + invoice.getGrossSales());
		customerManager.update(customer);
		
		// Log the creation
		logCreate(newInvoice);
		
		return newInvoice;
	}
	
	@Transactional
	@Override
	public SalesInvoice update(SalesInvoice invoice) throws RpcException {
		checkPermissions(UpdateSalesInvoice);
		
		SalesInvoice existing = checkExisting(invoice.getId());
		checkHasCompany(existing.getCompany());
		checkCanEditByStatus(existing, "sales invoice");
		
		// Update only updatable fields
		invoice.setStatus(existing.getStatus()); 
		
		SalesInvoice result = super.update(invoice);
		
		logUpdate(existing, result);
		
		return result;
	}
	
	@SuppressWarnings("unchecked")
	@Transactional
	@Override
	public SalesInvoice delete(SalesInvoice invoice) throws RpcException {
		checkPermissions(DeleteSalesInvoice);
		
		SalesInvoice existing = checkExisting(invoice.getId());
		checkHasCompany(existing.getCompany());
		checkCanDeleteByStatus(existing, "sales invoice");

		log.debug("Unlinking from delivery receipts...");
		ISimpleCriteria crit = SimpleCriteria.forClass(DeliveryReceipt.class);
		crit.eq("salesInvoice.id", existing.getId());
		List<DeliveryReceipt> receipts = getTemplate().findByCriteria(crit.getDetached());
		for (DeliveryReceipt receipt : receipts) {
			log.debug("Unlinking from {}.", receipt.getDrNo());
			receipt.setSalesInvoice(null);
			drManager.update(receipt);
		}
		
		SalesInvoice result = super.delete(existing);
		
		logDelete(result);
		
		return result;
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public List<SalesInvoice> getSalesInvoicesForCollection(Company company, Customer customer) throws RpcException {
		checkPermissions(SalesPermissions.ViewSalesInvoice);
		checkHasCompany(company); // Check if user is assigned to the company
		
		ISimpleCriteria crit = SimpleCriteria.forClass(SalesInvoice.class);
		crit.eq("company.id", company.getId());
		crit.eq("customer.id", customer.getId());
		crit.eq("status", TransactionStatus.POSTED);
		crit.gt("balance", 0.0d);
		crit.asc("date", "invoiceNo");
		List<SalesInvoice> list = getTemplate().findByCriteria(crit.getDetached());
		return list;
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public List<SalesInvoice> getSalesInvoicesUnderCollection(long collectionId) throws RpcException {
		checkPermissions(SalesPermissions.ViewSalesInvoice);
		
		Collection collection = collectionManager.getById(collectionId);
		if (collection == null) {
			log.warn("Collection with id={} was not found.", collectionId);
			throw new RpcException("The specified Collection does not exist!");
		}
		
		Company company = collection.getCompany();
		checkHasCompany(company); // Check if user is assigned to the company
		
		log.debug("Retrieving CollectionItems under Collection {}...", collection.getRefNo());
		ISimpleCriteria itemsCrit = SimpleCriteria.forClass(CollectionItem.class);
		itemsCrit.eq("collection.id", collection.getId());
		List<CollectionItem> items = getTemplate().findByCriteria(itemsCrit.getDetached());
		
		log.debug("Retrieving corresponding SalesInvoices...");
		ISimpleCriteria invoicesCrit = SimpleCriteria.forClass(SalesInvoice.class);
		invoicesCrit.in("id", CollectionUtils.listFrom(items, "getSalesInvoiceId"));
		List<SalesInvoice> list = getTemplate().findByCriteria(invoicesCrit.getDetached());
		
		return list;
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public PagingLoadResult<SalesInvoice> getList(TransactionLoadConfig config) throws RpcException {
		checkPermissions(SalesPermissions.ViewSalesInvoice);
		
		config.setCompanies(new ArrayList<Company>(getLoggedInUser().getCompanies()));

		ISimpleCriteria crit = TransactionHelper.createCriteria(SalesInvoice.class, config);
		List<SalesInvoice> list = (List<SalesInvoice>) TransactionHelper.getList(crit, config, getTemplate());
		
		ISimpleCriteria countCrit = TransactionHelper.createCountCriteria(SalesInvoice.class, config);
		Long count = (Long) getTemplate().findByCriteria(countCrit.getDetached()).get(0);
		
		PagingLoadResultBean<SalesInvoice> result = new PagingLoadResultBean<SalesInvoice>(list, count.intValue(), config.getOffset());
		return result;
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public List<SalesInvoice> getSalesInvoices(TransactionStatus status) throws RpcException {
		checkPermissions(SalesPermissions.ViewSalesInvoice);
		
		List<Company> companies = new ArrayList<Company>(getLoggedInUser().getCompanies());
		
		ISimpleCriteria crit = SimpleCriteria.forClass(SalesInvoice.class);
		crit.eq("status", status);
		crit.in("company.id", CollectionUtils.listFrom(companies, "getId"));
		List<SalesInvoice> list = getTemplate().findByCriteria(crit.getDetached());
		return list;
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public List<DeliveryReceipt> getDeliveryReceiptsByInvoice(long salesInvoiceId)  throws RpcException {
		checkPermissions(SalesPermissions.ViewDeliveryReceipt);
		
		SalesInvoice salesInvoice = getDataManager().getById(salesInvoiceId);
		if (salesInvoice == null) {
			log.warn("Sales Invoice with id={0} was not found.", salesInvoiceId);
			throw new RpcException("The specified Sales Invoice does not exist!");
		}
		
		Company company = salesInvoice.getCompany();
		checkHasCompany(company); // Check if user is assigned to the company
		
		log.debug("Retrieving Delivery Receipts under Sales Invoice {}...", salesInvoice.getInvoiceNo());
		ISimpleCriteria itemsCrit = SimpleCriteria.forClass(DeliveryReceipt.class);
		itemsCrit.eq("salesInvoice.id", salesInvoice.getId());
		List<DeliveryReceipt> deliveryReceipts = getTemplate().findByCriteria(itemsCrit.getDetached());
		return deliveryReceipts;
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public List<CollectionItem> getCollectionItemsByInvoice(long salesInvoiceId)  throws RpcException {
		checkPermissions(SalesPermissions.ViewCollection);
		
		SalesInvoice salesInvoice = getDataManager().getById(salesInvoiceId);
		if (salesInvoice == null) {
			log.warn("Sales Invoice with id={0} was not found.", salesInvoiceId);
			throw new RpcException("The specified Sales Invoice does not exist!");
		}
		
		Company company = salesInvoice.getCompany();
		checkHasCompany(company); // Check if user is assigned to the company
		
		log.debug("Retrieving Collection Items under Sales Invoice...");
		ISimpleCriteria itemsCrit = SimpleCriteria.forClass(CollectionItem.class);
		itemsCrit.eq("salesInvoice.id", salesInvoice.getId());
		List<CollectionItem> collectionItems = getTemplate().findByCriteria(itemsCrit.getDetached());
		return collectionItems;
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public List<SalesInvoice> getSalesInvoicesForAdvancePayment(long advancePaymentId) throws RpcException {
		checkPermissions(SalesPermissions.ViewSalesInvoice);
		
		AdvancePayment payment = advancePaymentManager.getById(advancePaymentId);
		if (payment == null) {
			log.warn("Advance Payment with id={0} was not found.", advancePaymentId);
			throw new RpcException("The specified Advance Payment does not exist!");
		}
		
		ISimpleCriteria crit = SimpleCriteria.forClass(SalesInvoice.class);
		crit.eq("status", TransactionStatus.POSTED);
		crit.in("collectionStatus", CollectionStatus.UNPAID, CollectionStatus.PARTIALLY_PAID);
		crit.eq("company.id", payment.getCompany().getId());
		crit.eq("customer.id", payment.getCustomer().getId());
		crit.geq("date", payment.getDate());
		
		List<SalesInvoice> list = getTemplate().findByCriteria(crit.getDetached());
		return list;
	}
	
	@Transactional
	@Override
	public List<SalesInvoice> postSalesInvoices(List<SalesInvoice> invoices) throws RpcException {
		checkPermissions(SalesPermissions.PostSalesInvoice);
		
		List<Long> ids = CollectionUtils.listFrom(invoices, "getId");
		List<SalesInvoice> existingInvoices = getDataManager().getByIds(SalesInvoice.COL_ID, ids);
		if (existingInvoices.isEmpty())
			return existingInvoices;
		
		List<SalesInvoice> results = new ArrayList<SalesInvoice>(existingInvoices.size());
		
		for (SalesInvoice existing : existingInvoices) {
			checkHasCompany(existing.getCompany());
			
			if (existing.getStatus() == TransactionStatus.POSTED) {
				log.debug("Sales Invoice '{}' has been posted. This will be skipped.", existing.getInvoiceNo());
				continue;
			}

			log.debug("Posting sales invoice {}...", existing.getInvoiceNo());
			existing.setStatus(TransactionStatus.POSTED);
			SalesInvoice result = getDataManager().update(existing);
			
			// Create TransactionEntry
			log.debug("Creating transaction entry for sales invoice {}...", existing.getInvoiceNo());
			TransactionEntry t = createForGross(existing);
			transactionEntryManager.create(t);
			
			logUpdate(existing, result);
			
			results.add(result);
		}
		
		return results;
	}
	
	// Static Helpers %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
	
	private static TransactionEntry createForGross(SalesInvoice invoice) {
		TransactionEntry te = new TransactionEntry();
		te.setCompany(invoice.getCompany());
		te.setDate(invoice.getDate());
		te.setType(TransactionType.SALES_INVOICE);
		te.setTransactionId(invoice.getId());
		te.setTransactionRefNo(invoice.getGroupAndNo());
		te.setCustomer(invoice.getCustomer());
		te.setSalesman(invoice.getSalesman());
		te.setProduct(invoice.getProduct());
		te.setDebit(invoice.getGrossSales());
		te.setCredit(0d);
		return te;
	}
	
}
